import AliOSS from 'ali-oss'
import RawaOSS, { RawaOssMultipartUploadResult } from '@/tools/rawa-oss'
import { Vue, Component, Prop } from 'vue-property-decorator'

type OSS = AliOSS | RawaOSS
class Reducer<T> {
  constructor (cb: (data: T) => void, fn: (...args: any[]) => Promise<T>, ...args: any[]) {
    this.cb = cb
    this.fn = fn
    this.args = args
  }

  cb!: (data: T) => void

  fn!: (...args: any[]) => Promise<T>

  args!: any[]

  loading = false

  queue: Array<{ resolve: (data: T) => void; reject: (e?: Error) => void}> = []

  trigger (): Promise<T> {
    if (this.loading) {
      return new Promise((resolve, reject) => {
        this.queue.push({ resolve, reject })
      })
    } else {
      this.loading = true
      return this.fn(...this.args).then((data: T) => {
        if (this.cb) this.cb(data)
        this.queue.forEach(v => {
          v.resolve(data)
        })
        return data
      }).catch(e => {
        this.queue.forEach(v => {
          v.reject(e)
        })
        return Promise.reject(e)
      }).finally(() => {
        this.queue = []
        this.loading = false
      })
    }
  }
}

export interface OssConfig {
  accessKeyId: string;

  accessKeySecret: string;

  securityToken: string;

  expiration: string;

  requestId: string;

  bucket: string;

  endpoint: string;

}

export interface UploadParams {
  name: string;
  file: File;
  path: string;
  actualPath: string;
  parentId: number;
  beforeUpload: (params: UploadParams, queue: FileData[]) => Promise<any>;
  afterUpload: (params: UploadParams, resp: UploadResp, queue: FileData[]) => Promise<any>;
}

export interface UploadResp {
  name: string;
  url: string;
  data: any;
  message?: string;
}

export class FileData {
  constructor (params: UploadParams) {
    this.params = params
  }

  params: UploadParams;

  resp: UploadResp | null = null;

  progress = 0;

  status: 'waiting' | 'uploading' | 'success' | 'error' | 'cancel' | 'abort' = 'waiting';

  client: OSS | null = null;

  checkpoint: any = null;

  dataSource: OssDataSource = 'rawa'
}

export type OssDataSource = 'rawa' | 'aliyun'
export interface OssOptions {
  loadConfigFn: (dataSource: OssDataSource) => Promise<OssConfig>;
}

let uid = 1

@Component
export default class Oss extends Vue {
  @Prop() options!: OssOptions

  queue: FileData[] = []

  maxUploading = 5

  configMap: Partial<Record<OssDataSource, OssConfig>> = {}

  tokenReducerMap: Record<OssDataSource, Reducer<OssConfig>> = {
    rawa: new Reducer<OssConfig>((data) => { this.configMap.rawa = data }, this.options.loadConfigFn, 'rawa'),
    aliyun: new Reducer<OssConfig>((data) => { this.configMap.aliyun = data }, this.options.loadConfigFn, 'aliyun')
  }

  // 文件管理器显示/隐藏
  visible = false

  private tokenHasExpired (dataSource: OssDataSource) {
    const config = this.configMap[dataSource]
    if (!config) return true
    if (dataSource === 'rawa') return false
    const expiration = new Date(config.expiration)
    return Date.now() - expiration.getTime() > 60 * 1000
  }

  private loadConfig (dataSource: OssDataSource): Promise<OssConfig> {
    const fn = this.options?.loadConfigFn
    if (!fn) throw new Error('未配置OSS loadConfigFn')
    if (this.tokenHasExpired(dataSource)) {
      return this.tokenReducerMap[dataSource].trigger()
    } else {
      return Promise.resolve(this.configMap[dataSource] as OssConfig)
    }
    // return this.options.loadConfigFn(dataSource)
  }

  private getContext (dataSource: OssDataSource): Promise<{ client: OSS; config: OssConfig}> {
    return this.loadConfig(dataSource).then(config => {
      const Inst = dataSource === 'rawa' ? RawaOSS : AliOSS
      const client = new Inst({
        endpoint: config?.endpoint,
        accessKeyId: config?.accessKeyId,
        accessKeySecret: config?.accessKeySecret,
        stsToken: config?.securityToken,
        bucket: config?.bucket
      })
      return { client, config }
    })
  }

  private scheduleUpload () {
    const handler = setInterval(() => {
      if (this.queue.filter(v => v.status === 'waiting').length < 1 || !this.$auth.authenticated) return
      const num = this.queue.filter(v => v.status === 'uploading').length
      if (num >= this.maxUploading) return
      this.queue.filter(v => v.status === 'waiting').some((v, i) => {
        if (i >= this.maxUploading - num) return true
        v.status = 'uploading'
        this.getContext(v.dataSource).then(({ client, config }) => {
          const path = config.requestId + `-${uid++}/` + encodeURIComponent(v.params.name)
          v.client = client
          ;(client.multipartUpload(path, v.params.file, {
            progress: function (p, checkpoint) {
              v.progress = Number(Number(p * 100).toFixed(1))
              v.checkpoint = checkpoint
            },
            checkpoint: v.checkpoint || undefined
          }) as Promise<RawaOssMultipartUploadResult>).then((resp: any) => {
            const data = {
              name: resp.name,
              url: (resp.res as any)?.requestUrls[0],
              data: resp
            }
            return v.params.afterUpload(v.params, data, this.queue).then(() => {
              v.progress = 100
              v.status = 'success'
              v.resp = data
            })
          }).catch((e: any) => {
            if ((client as any).isCancel()) {
              if (v.status !== 'abort') {
                v.status = 'cancel'
              }
            } else if (e.name === 'abort') {
              v.status = 'abort'
              v.progress = 0
              v.checkpoint = null
            } else {
              v.status = 'error'
              v.resp = { name: path, url: '', message: e.message || e?.data?.message, data: e }
              console.error(e)
            }
          })
        }).catch((e: any) => {
          v.status = 'waiting'
        })
      })
    }, 1000)
    return () => {
      clearInterval(handler)
    }
  }

  upload (params: UploadParams) {
    params.beforeUpload(params, this.queue).then(() => {
      const item = new FileData(params)
      // this.queue.splice(0, 0, item)
      this.queue.push(item)
      this.visible = true
    })
  }

  signatureUrl (url: string, response: any = {}): Promise<string> {
    const dataSource: OssDataSource = new URL(url).hostname.indexOf('aliyun') !== -1 ? 'aliyun' : 'rawa'
    return this.getContext(dataSource).then(({ client, config }) => {
      const name = new URL(url).pathname.slice(1)
      return client.signatureUrl(name, response)
      // const parts = name.split('/')
      // const last = parts.pop()
      // if (last) {
      //   parts.push(encodeURIComponent(last))
      // }
      // return client.signatureUrl(parts.join('/'), response)
    })
  }

  cancel (fileData: FileData) {
    if (!fileData.client || fileData.status !== 'uploading' || !fileData.checkpoint) throw new Error('该状态不能暂停上传')
    ;(fileData.client as any).cancel()
  }

  abort (fileData: FileData) {
    if (['waiting'].includes(fileData.status)) {
      fileData.status = 'abort'
      return
    }
    if (!fileData.client || !['uploading', 'cancel'].includes(fileData.status) || !fileData.checkpoint) throw new Error('该状态不能终止上传')
    fileData.client.abortMultipartUpload(fileData.checkpoint?.name, fileData.checkpoint?.uploadId)
    fileData.status = 'abort'
    fileData.checkpoint = null
    fileData.progress = 0
  }

  resume (fileData: FileData) {
    fileData.status = 'waiting'
  }

  created () {
    const fn = this.scheduleUpload()
    this.$once('hook:beforeDestroy', function () {
      fn()
    })

    // const genParams = () => {
    //   return {
    //     name: '支持预览的文件格式.docx',
    //     file: { name: '支持预览的文件格式.docx', size: Math.floor(Math.random() * 1000000000) },
    //     path: '我的文档/预览/',
    //     parentId: 0,
    //     beforeUpload: (params: UploadParams, queue: FileData[]) => Promise.resolve(null),
    //     afterUpload: (resp: UploadResp, queue: FileData[]) => Promise.resolve(null)
    //   }
    // }
    // const t1 = new FileData(genParams() as any)
    // const t2 = new FileData(genParams() as any)
    // t2.status = 'abort'
    // const t3 = new FileData(genParams() as any)
    // t3.status = 'cancel'
    // t3.progress = 60
    // const t4 = new FileData(genParams() as any)
    // t4.status = 'error'
    // t4.resp = {
    //   name: '',
    //   url: '',
    //   data: {},
    //   message: '服务器错误, 网络连接拒绝'
    // }
    // const t5 = new FileData(genParams() as any)
    // t5.status = 'success'
    // const t6 = new FileData(genParams() as any)
    // t6.status = 'uploading'
    // t6.progress = 40
    // this.queue.push(...[t1, t2, t3, t4, t5, t6])
  }
}
